iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0
Software Development

學 Python 到底可以幹麻勒?系列 第 23

( Day 23 ) 影片加文字、影片自動加字幕

  • 分享至 

  • xImage
  •  

這篇文章會介紹使用 Python 的 moviepy 第三方函式庫讀取影片,搭配 Pillow 函式庫在影片中加入中文與英文字,此外,也會把外部字幕檔案轉換成字卡,將字幕字卡與影片進行合成,實作影片自動加上字幕。 ( 因為 moviepy 內建方法 TextClip 還要額外安裝 ImageMagick,所以使用 Pillow 加入文字 )。

原文參考:影片中加入文字影片自動加上字幕

本篇使用的 Python 版本為 3.7.12,所有範例可使用 Google Colab 實作,不用安裝任何軟體 ( 參考:使用 Google Colab )

Python 教學 - 影片自動加上字幕

安裝 moviepy 和 Pillow

輸入下列指令安裝 moviepy,根據個人環境使用 pip 或 pip3。

!pip install moviepy

輸入下列指令安裝 Pillow,根據個人環境使用 pip 或 pip3,如果使用 Colab 或 Anaconda Jupyter,已經內建 Pillow 函式庫。

!pip install Pillow

由於影片轉檔會使用 ffmpeg,因此也要安裝 ffmpeg ( 影片存檔常見錯誤「TypeError: must be real number, not NoneType」往往都是 ffmpeg 沒有安裝導致 ),根據個人環境使用 pip 或 pip3,Anaconda Jupyter 可以使用 conda install。

!pip install ffmpeg

使用 Pillow 產生文字字卡

載入 PIL 的 Image、ImageFont 和 ImageDraw 模組,按照下列步驟建立背景透明的文字字卡:

  • 使用 Image.new 建立色彩模式為 RGBA,尺寸 360x180 的空白圖片。
  • 使用 ImageFont.truetype 設定文字的字型和尺寸 ( 字型使用 Google Font 的 NotoSansTC-Regular )。
  • 使用 ImageDraw.Draw 建立繪圖物件。
  • 使用 text 方法在圖片中寫入文字 ( 參數說明:ImageDraw.text )。
import os
os.chdir('/content/drive/MyDrive/Colab Notebooks')  # 使用 Colab 要換路徑使用

from PIL import Image, ImageFont, ImageDraw

img = Image.new('RGBA', (360, 180))                       # 建立色彩模式為 RGBA,尺寸 360x180 的空白圖片
font = ImageFont.truetype('NotoSansTC-Regular.otf', 40)   # 設定字型與尺寸
draw = ImageDraw.Draw(img)                                # 準備在圖片上繪圖
# 將文字畫入圖片
draw.text((10,120),'OXXO.STUDIO',fill=(255,255,255),font=font,stroke_width=2,stroke_fill='red')
draw.text(xy=(50,0), text='大家好\n哈哈', align='center', fill=(255,255,255), font=font, stroke_width=2, stroke_fill='blue')
img.save('ok.png')    # 儲存為 png

Python 教學 - 影片中加入文字

合成影片與文字

使用 moviepy 讀取影片後,按照下列步驟將文字加入影片中:

  • 使用 resize 將影片尺寸調整為字卡的大小 ( 也可以在產生字卡圖片時,做成和影片同樣大小,就不需這個步驟 )。
  • 使用 subclip 剪輯出兩秒的長度 ( 看個人需求,範例使用兩秒 )。
  • 使用 ImageClip 和 set_duration 將靜態圖片建立為長度兩秒的影片物件 ( transparent=True 設定背景透明 )。
  • 使用 CompositeVideoClip 將兩段影片混合。
import os
os.chdir('/content/drive/MyDrive/Colab Notebooks')  # 使用 Colab 要換路徑使用

from moviepy.editor import *
from PIL import Image, ImageFont, ImageDraw

img = Image.new('RGBA', (360, 180))
font = ImageFont.truetype('NotoSansTC-Regular.otf', 40)
draw = ImageDraw.Draw(img)
draw.text((10,120), 'OXXO.STUDIO', fill=(255,255,255), font=font, stroke_width=2, stroke_fill='red')
draw.text(xy=(50,0), text='大家好\n哈哈', align='center', fill=(255,255,255), font=font, stroke_width=2, stroke_fill='blue')
img.save('ok.png')

video = VideoFileClip("baby_shark.mp4")         # 讀取影片
clip = video.resize((360,180)).subclip(10,12)   # 縮小影片尺寸,剪輯出 10~12 秒的片段
text_clip = ImageClip("ok.png", transparent=True).set_duration(2)   # 讀取圖片,將圖片變成長度兩秒的影片

output = CompositeVideoClip([clip, text_clip])  # 混合影片
output.write_videofile("output.mp4",temp_audiofile="temp-audio.m4a", remove_temp=True, codec="libx264", audio_codec="aac")

print('ok')

Python 教學 - 影片中加入文字

讀取字幕,轉換成組合用的串列

延伸「下載 Youtube 影片 ( mp4、mp3、字幕 )」文章,下載影片與字幕檔,下載後開啟字幕檔案,觀察字幕檔案的構成方式 ( 也可以用一些網路的線上服務下載,範例使用 baby shark ):

Python 教學 - 影片自動加上字幕

撰寫下方的程式,將字幕檔案的內容,根據字串拆分的規則,轉換成「時間串列」和「字幕文字串列」,其中時間串列需要將「時分秒」轉換成「總秒數」,詳細說明標注在程式碼中:

相關教學參考:重複迴圈 ( for、while )邏輯判斷 ( if、elif、else )

import os
os.chdir('/content/drive/MyDrive/Colab Notebooks')  # 使用 Colab 要換路徑使用

# 定義轉換為總秒數的函式
def time2sec(t):
    arr = t.split(' --> ')   # 根據「' --> '」拆分文字
    s1 = arr[0].split(',')   # 前方的文字為開始時間
    s2 = arr[1].split(',')   # 後方的文字為結束時間
    # 計算開始時間的總秒數
    start = int(s1[0].split(':')[0])*3600 + int(s1[0].split(':')[1])*60 + int(s1[0].split(':')[2]) + float(s1[1])*0.001
    # 計算結束時間的總秒數
    end = int(s2[0].split(':')[0])*3600 + int(s2[0].split(':')[1])*60 + int(s2[0].split(':')[2]) + float(s2[1])*0.001
    return [start, end]      # 回傳開始時間與結束時間的串列

f = open('oxxostudio.srt','r')  # 使用 open 方法的 r 開啟字幕檔案
srt = f.read()                  # 讀取字幕檔案內容
f.close()                       # 關閉檔案
srt_list = srt.split('\n')      # 將內容根據換行符號 \n 拆分成串列
sec = 1                         # 串列中秒數從第二項開始 ( 串列的第二項的索引值為 1 )
text = 2                        # 串列中文字內容從第三項開始 ( 串列的第三項的索引值為 2 )
sec_list = [[0,0]]              # 定義時間串列的開頭為 [0,0]
text_list = ['']                # 定義字幕內容串列的開頭為空字串 ''
# 使用迴圈,讀取字幕檔案串列的每個項目
for i in range(len(srt_list)):
    if i == sec:
        sec = sec + 4           # 如果遇到時間內容,就將 sec + 4 ( 因為時間每隔 4 個項目會出現 )
        # 如果兩個串列項目內容前後對不上 ( 前一個結束時間不等於後一個的開始時間 )
        if sec_list[-1][1] != time2sec(srt_list[i])[0]:
            # 在時間串列中間添加一個開始時間與結束時間內容 ( 表示該區間沒有字幕 )
            sec_list.append([sec_list[-1][1],time2sec(srt_list[i])[0]])
            # 在文字串列中間添加一個空字串 ( 表示該區間沒有字幕 )
            text_list.append('')
        sec_list.append(time2sec(srt_list[i]))  # 添加時間到時間串列
    if i == text:
        text = text + 4               # 如果遇到文字內容,就將 text + 4 ( 因為文字每隔 4 個項目會出現 )
        text_list.append(srt_list[i]) # 添加文字到文字串列

print(sec_list)
print(text_list)

Python 教學 - 影片自動加上字幕

合併影片與字幕

將字幕串列與時間串列組合,產生文字字卡,並根據時間串列切割原始影片,將影片片段與字卡組合,就能輸出為帶有字幕的影片了,詳細說明寫在程式碼中:

import os
os.chdir('/content/drive/MyDrive/Colab Notebooks')  # 使用 Colab 要換路徑使用

from moviepy.editor import *
from PIL import Image, ImageFont, ImageDraw

def time2sec(t):
    arr = t.split(' --> ')
    s1 = arr[0].split(',')
    s2 = arr[1].split(',')
    start = int(s1[0].split(':')[0])*3600 + int(s1[0].split(':')[1])*60 + int(s1[0].split(':')[2]) + float(s1[1])*0.001
    end = int(s2[0].split(':')[0])*3600 + int(s2[0].split(':')[1])*60 + int(s2[0].split(':')[2]) + float(s2[1])*0.001
    return [start, end]

f = open('oxxostudio.srt','r')
srt = f.read()
f.close()
srt_list = srt.split('\n')
#print(text_list)
sec = 1
text = 2
srt_list = [[0,0]]
text_list = ['']
for i in range(len(srt_list)):
    if i == sec:
        sec = sec + 4
        if sec_list[-1][1] != time2sec(srt_list[i])[0]:
            sec_list.append([sec_list[-1][1],time2sec(srt_list[i])[0]])
            text_list.append('')
        sec_list.append(time2sec(srt_list[i]))
    if i == text:
        text = text + 4
        text_list.append(srt_list[i])

print(sec_list)
print(text_list)

img_empty = Image.new('RGBA', (480, 240))                 # 產生 RGBA 空圖片
font = ImageFont.truetype('NotoSansTC-Regular.otf', 20)   # 設定文字字體和大小
video = VideoFileClip("baby_shark.mp4").resize((480,240)) # 讀取影片,改變尺寸
video_duration = float(video.duration)                    # 讀取影片總長度
output_list = []                                          # 記錄最後要組合的影片片段

# 如果字幕最後的時間小於總長度
if sec_list[-1][1] != video_duration:
    sec_list.append([sec_list[-1][1],video_duration])     # 添加時間到時間串列
    text_list.append('')                                  # 添加空字串到文字串列

# 建立文字字卡函式
def text_clip(text, name):
    img = img_empty.copy()      # 複製空圖片
    draw = ImageDraw.Draw(img)  # 建立繪圖物件,並寫入文字
    text_width = 21*len(text)   # 在 480x240 文字大小 20 狀態下,一個中文字長度約 21px
    draw.text(((480-text_width)/2,10), text, fill=(255,255,255), font=font, stroke_width=2, stroke_fill='black')
    img.save(name)              # 儲存

# 建立影片和文字合併的函式
def text_in_video(t, text_img):
    clip = video.subclip(t[0],t[1])                  # 剪輯影片到指定長度
    text = ImageClip(text_img, transparent=True).set_duration(t[1]-t[0])  # 讀取字卡,調整為影片長度
    combine_clip = CompositeVideoClip([clip, text])  # 合併影片和文字
    output_list.append(combine_clip)                 # 添加到影片片段裡

# 使用 for 迴圈,產生文字字卡
for i in range(len(text_list)):
    text_clip(text_list[i], 'srt.png')
    text_in_video(sec_list[i], 'srt.png')

output = concatenate_videoclips(output_list)      # 合併所有影片片段
output.write_videofile("output.mp4",temp_audiofile="temp-audio.m4a", remove_temp=True, codec="libx264", audio_codec="aac")
print('ok')

執行程式後,就會產生一個 480x240 帶有字幕的影片 ( 下圖的影片截圖上方,有中文字幕 )。

Python 教學 - 影片自動加上字幕

更多 Python 教學

大家好,我是 OXXO,是個即將邁入中年的斜槓青年,我已經寫了超過 400 篇 Python 的教學,有興趣可以參考下方連結呦~ ^_^


上一篇
( Day 22 ) 調整影片亮度、對比、顏色、速度、倒轉影片
下一篇
( Day 24 ) 影片轉換為 gif 動畫
系列文
學 Python 到底可以幹麻勒?41
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

我要留言

立即登入留言